const URL = require('url');
const axios = require('axios');
const websocket = require("ws");

class KookBot{

    guildList = [];
    wsClient = null;
    exit = false;
    sn = 0;

    constructor(BOT_TOKEN, BOT_ID){
        this.botID = BOT_ID;
        this.axios = axios.create({
            baseURL: 'https://www.kookapp.cn',
            params: {
                compress: 0
            },
            headers: {
              Authorization: `Bot ${BOT_TOKEN}`
            }
        });
    };

    async getCommon(url, data){
        return new Promise((resolve, reject) => {
            let timeOut = setTimeout(() => {
                console.log("[INFO] --KOOK-- GET Timeout. " + url + data);
                reject(new TypeError("[INFO] --KOOK-- GET Timeout."));
            }, 10000);
            if (typeof data != 'object' && data !== undefined)
                reject("Type_Err: Not an Object.")
            this.axios.get(`/api/v3${url}${URL.format({ query: data })}`)
                .then(res => {
                    clearTimeout(timeOut);
                    resolve(res.data.data);
                })
                .catch(err => reject(err.response.data))
        });
    }

    async postCommon(url, data){
        return new Promise((resolve, reject) => {
            let timeOut = setTimeout(() => {
                console.log("[INFO] --KOOK-- POST Timeout. " + url + JSON.stringify(data));
                reject(new TypeError("Post_Timeout."));
            }, 10000);
            if (typeof data != 'object' && data !== undefined)
                reject("Type_Err: Not an Object.")
            this.axios.post('/api/v3' + url, data)
                .then(res => {
                    clearTimeout(timeOut);
                    resolve(res.data.data);
                })
                .catch(err => reject(err.response.data))
        });
    };

    async getGuildList(){
        let guild_list_data = await this.getCommon('/guild/list');
        this.guildList = guild_list_data.items;
    };

    async sendMsg(msg, target_id, type){
        return new Promise((resolve, reject) => {
            if(!msg) reject ("Ref_Err: Null value.");
            if(typeof msg === 'object') msg = JSON.stringify(msg);
            if(typeof msg != 'string') reject ("Type_Err: Not a String.");
            this.postCommon('/message/create', {
                type: type ?? 1,
                target_id,
                content: msg,
            })
                .then((res)=>resolve(res))
                .catch((err)=>reject(err));
        });
    }

    async sendCardMsg(msg, target_id){
        return this.sendMsg(msg, target_id, 10);
    }

    async updateMsgById(msg_id, content){
        if(!msg_id) return Promise.reject("Ref_Err: Null value.");
        if(!content) return this.deleteMsgById(msg_id);
        if(typeof content === 'object') content = JSON.stringify(content);
        if(typeof content != 'string') return Promise.reject ("Type_Err: Not a String.");
        return this.postCommon('/message/update', {
            msg_id,
            content
        });
    }

    async deleteMsgById(msg_id){
        if(!msg_id) return Promise.reject("Ref_Err: Null value.");
        return this.postCommon('/message/delete', {msg_id});
    }

    async getWsURL(){
        return new Promise((resolve, reject) => {
            this.getCommon('/gateway/index')
                .then(res=>resolve(res.url))
                .catch(err=>reject(err));
        })
    }

    async getVoiceWsURL(channel_id){
        return new Promise(async (resolve, reject) => {
            this.getCommon('/gateway/voice', { channel_id })
                .then(res => {
                    resolve(res.gateway_url);
                })
                .catch(err => reject(err));
        });
    };

    async getJoinChannelID(guild_id, user_id){
        return new Promise((resolve, reject) => {
            this.getCommon('/channel-user/get-joined-channel', { guild_id, user_id })
                .then(res => resolve(res.items[0]))
                .catch(err => reject(err));
        });
    };

    async connectWebSocket(callback){
        this.close = false;
        this.wsClient = new websocket(await this.getWsURL());
        this.wsClient.on('open', ()=>{
            this.wsHeartBeat = setInterval(()=>{
                this.wsClient.send(JSON.stringify({s: 2, sn: this.sn ?? 0}));
            }, 30000);
            console.log('[INFO] --KOOK-- ws connected');
            callback;
        });
        this.wsClient.on('close', ()=>{
            console.log('[INFO] --KOOK-- ws disconnected');
            if(this.exit === false){
                console.log('[INFO] --KOOK-- reconnecting...');
                this.wsClient = null;
                this.connectWebSocket();
            };
        });

        this.wsClient.on('message', async res =>{
            let data = JSON.parse(res);
            let heatBeatTimeout = null;

            switch(data.s){
                case 0:// Message   server -> client
                    this.sn = data.sn;
                    this.msgHandler(data.d) // abstract
                        .catch(err=>this.errorHandler(err));
                    break;
                case 1:// Hello     server -> client
                    break;
                case 2:// Ping      client -> server
                    heatBeatTimeout = setTimeout(()=>{
                        console.log('[EXCP] --KOOK-- HeartBeatTimeout');
                    },6000);
                    break;
                case 3:// Pong      server -> client
                    clearTimeout(heatBeatTimeout);
                case 4:
                    break;
                case 5:
                    console.log("[INFO] --KOOK-- Force disconnect recv from Server.");
                    await this.reconnectWebsocket().catch(err=>console.log(err));
                    break;
                default:
                    console.log('[INFO] --KOOK-- NewMsg: ' + data.s);
            };
        });
    };

    async disconnectWebsocket(){
        return new Promise((resolve, reject)=>{
            if(!this.wsClient) reject('[EXCP] --KOOK-- Websocket is not connected');
            this.exit = true;
            clearInterval(this.wsHeartBeat);
            this.wsClient.on('close', async ()=>{
                delete this.wsClient;
                this.sn = null;
                await this.offLine();
                resolve();
            });
            this.wsClient.close();
        });
    }

    async reconnectWebsocket(){
        return new Promise((resolve, reject)=>{
            this.disconnectWebsocket().catch(err => reject(err))
                .then(async ()=>{
                    await this.delay(5000);
                    this.connectWebSocket().catch(err => reject(err))
                        .then(resolve);
                });
        });    
    }

    async postNowPlayingStatus(artist, music_name){
        return new Promise((resolve, reject)=>{
            this.postCommon('/game/activity', {
                datatype: 2,
                singer: artist,
                music_name
            })  .then(()=>resolve())
                .catch(err => reject(err));
        })
    }

    async stopNowPlayingStatus(){
        return new Promise((resolve, reject) => {
            this.postCommon('/game/delete-activity', {
                datatype: 2
            }).then(() => resolve())
                .catch(err => reject(err));
        })
    }

    async switchNowPlayingStatus(artist, music_name){
        return new Promise((resolve, reject) => {
            this.stopNowPlayingStatus()
                .then(()=>{
                    this.postNowPlayingStatus(artist, music_name)
                        .then(()=>resolve())
                        .catch(err => reject(err));
                })
                .catch(err => reject(err));
        });
    }

    async offLine(){
        await this.postCommon('/user/offline');
    };

    async delay(ms){
        return new Promise(resolve=>setTimeout(resolve,ms));
    }

    async errorHandler(err, target_id){
        console.log(err);
    }
}

module.exports = KookBot;


// this.voiceClient.send(JSON.stringify({
    //     request: true,
    //     id: voiceConnectionID,
    //     method: "createWebRtcTransport",
    //     data: {
    //         forceTcp: false,
    //         producing: true,
    //         consuming: false,
    //         sctpCapabilities: {
    //             numStreams: {
    //                 OS:1024,
    //                 MIS:1024
    //             }
    //         }
    //     }
    // }));